﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Services;
using System.Net;
using System.Text.RegularExpressions;
using System.Drawing;
using System.Threading;
using System.Windows.Threading;
using System.IO;

namespace Faust.Andrew.MTG
{
    public class WebGatherer : Gatherer
    {        
        //Listing Page Scrape Information        
        private string _url = "http://gatherer.wizards.com/Pages/Search/Default.aspx?output=standard&special=true&action=advanced";
        private string _pageLinksParameter = @"/Pages/Search/Default.aspx\?page=(\d+)";
        private string _pageParameter = "&page={0}";
        private string _colorParameter = "&color=+[{0}]";
        private string _typeParameter = "&type=+[\"{0}\"]";
        private string _rarityParameter = "&rarity=+[{0}]";
        private string _textParameter = "&text=+[\"{0}\"]";
        private string _nameParameter = "&name=+[\"{0}\"]";
        private string _subtypeParameter = "&subtype=+[\"{0}\"]";
        private string _expansionParameter = "&set=|[\"{0}\"]";
        private string _sortParameter = "&sort={0}";
        private string _multiverseIdRegEx = @"Details.aspx\?multiverseid=(\d+)";
        private string _cardImageUrl = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid={0}&type=card";

        //Detail Page Scrape Information
        private string _detailUrl = "http://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=";
        private string _nameRegEx = "Card Name\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*([^\\<]+)<";
        private string _convertedManaCostRegEx = "Converted Mana Cost\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*([^\\<]+)<";
        private string _typesRegEx = "Types\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*([^\\<]+)<";
        //private string _powerToughnessRegEx = "P/T\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*(?<Power>[^\\<]+) / (?<Toughness>[^\\<]+)<";
        private string _powerRegEx = "P/T\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*([^\\<]+) / [^\\<]+<";
        private string _toughnessRegEx = "P/T\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*([^\\<]+) / [^\\<]+<";
        private string _rarityRegEx = "Rarity\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>\\s*<span class=[^>]+.\\s*([^<]+)<";
        private string _expansionRegEx = "<a href=\"/Pages/Search/Default.aspx\\?action=advanced\\&amp;set=\\[%22.+%22]\">(.+)</a>";

        //Detail Page Scrape Information Which Requires Further Processing
        private string _cardTextStartRegEx = "<div class=\"cardtextbox\"\\>";
        private string _divEndRegEx = "</div>";
        private string _manaCostStartRegEx = "Mana Cost\\:\\s*\\</div\\>\\s*\\<div class=\"value\"\\>";

        //Generic Detail Page Scrape Regular Expressions
        private string _imageAltTextRegEx = "<img [^>]*alt=\"([^\"]+)\"[^>]*>";
        private string _imageAltReplacementRegEx = "$1 ";  
        
        //Planes Chaos Image Text. For some reason the standard AltTextReplacement didn't work.
        private string _chaosImageReplacement = "<img src='/Images/Symbols/chaos.gif' alt='[chaos]' />";
        
        //We spend way too much time retrieving all the images for basic land. To fix this, I'm going to one basic land per type
        //Due to the current architecture, it's easier to do this via an exclusion list of multiverse ids to reject.
        //This means any new lands added after this list was generated will show up. That's OK, the quantity will be small enough
        //to be reasonable.
        //The net result is that it will be impossible for a person to select the exact land they want out of all versions.
        //I'm sure this will bother someone, somewhere at some point. If so, that person is way too obsesive.
        /*
         * These are the lands being allowed (they are not in the exclusion list)
            private const string _swampMultiverseId = "191391";
            private const string _islandMultiverseId = "191400";
            private const string _plainsMultiverseId = "191385";
            private const string _mountainMultiverseId = "191401";
            private const string _forestMultiverseId = "191407";
         * */
        private List<string> _basicLandExclusionList;


        public WebGatherer(bool excludeNonBasicLand)
            : base()
        {
            if (excludeNonBasicLand)
            {
                LoadLandExclusionList();
            }
            else
            {
                _basicLandExclusionList = new List<string>();
            }
        }

        public WebGatherer() : base()
        {
            LoadLandExclusionList();
        }

        private void LoadLandExclusionList()
        {
            _basicLandExclusionList = new List<string>();

            using (TextReader reader = GetLandExclusionTextReader())
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (!String.IsNullOrEmpty(line) && !_basicLandExclusionList.Contains(line))
                    {
                        _basicLandExclusionList.Add(line);
                    }
                }
            }                        
        }

        private TextReader GetLandExclusionTextReader()
        {
            string resourceName = "Faust.Andrew.MTG.LandExclusions.txt";
            return new StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName));
        }

        protected string GetColorSearchValue(string color)
        {
            string tempColor = color.ToUpper();

            switch (tempColor)
            {
                case "RED":
                    return "R";
                case "BLUE":
                    return "U";
                case "BLACK":
                    return "B";
                case "GREEN":
                    return "G";
                case "WHITE":
                    return "W";
                case "COLORLESS":
                    return "C";
                default:
                    return color;
            }
        }

        protected string GetRaritySearchValue(string rarity)
        {
            string tempRarity = rarity.ToUpper();

            switch (tempRarity)
            {
                case "RARE":
                    return "R";
                case "UNCOMMON":
                    return "U";
                case "COMMON":
                    return "C";
                case "MYTHIC RARE":
                    return "M";
                case "LAND":
                    return "L";
                default:
                    return rarity;
            }
        }

        public override CardSearchResults FindCards(CardSearchParameters searchParameters)
        {
            return FindCards(searchParameters, true);
        }

        private string SortEnumToSearchString(SortOrder sortOrder)
        {
            //+ = Sort Ascending
            //- = Sort Descending
            switch (sortOrder)
            {
                case SortOrder.Color:
                    return "color+"; 
                case SortOrder.Cost:
                    return "cmc+";
                case SortOrder.Name:
                    return "abs+";
                default:
                    return "rarity-";
            }
        }

        private string GetSearchUrl(CardSearchParameters parameters)
        {
            StringBuilder searchUrl = new StringBuilder();
            //string fullUrl = _url + String.Format(_pageParameter, 0) + String.Format(_nameParameter, searchParameters.CardName);
            searchUrl.Append(_url);
            
            //Make sure the caller isn't requesting a negative page number
            int requestedPage = 0;
            if (parameters.Page >= 0)
            {
                requestedPage = parameters.Page;
            }

            searchUrl.AppendFormat(_pageParameter, requestedPage);
            
            if (!String.IsNullOrEmpty(parameters.Name)) { searchUrl.AppendFormat(_nameParameter, parameters.Name); }
            if (!String.IsNullOrEmpty(parameters.Color)) { searchUrl.AppendFormat(_colorParameter, GetColorSearchValue(parameters.Color)); }
            if (!String.IsNullOrEmpty(parameters.Rarity)) { searchUrl.AppendFormat(_rarityParameter, GetRaritySearchValue(parameters.Rarity)); }
            if (!String.IsNullOrEmpty(parameters.Type)) { searchUrl.AppendFormat(_typeParameter, parameters.Type); }
            if (!String.IsNullOrEmpty(parameters.Text)) { searchUrl.AppendFormat(_textParameter, parameters.Text); }
            if (!String.IsNullOrEmpty(parameters.SubType)) { searchUrl.AppendFormat(_subtypeParameter, parameters.SubType); }
            if (!String.IsNullOrEmpty(parameters.Expansion)) { searchUrl.AppendFormat(_expansionParameter, parameters.Expansion); }                                

            searchUrl.AppendFormat(_sortParameter, SortEnumToSearchString(parameters.SortOrder));

            return searchUrl.ToString();
        }

        private CardSearchResults FindCardsById(string multiverseId, bool loadDetails)
        {
            CardSearchResults results = new CardSearchResults();

            Card card = new Card();
            card.MultiverseID = multiverseId;
            results.Cards.Add(card);
            results.Page = 0;
            results.Pages = 0;
            results.TotalCards = 1;

            if (loadDetails)
            {
                LoadCardDetails(results.Cards);
            }

            return results;
        }

        public override CardSearchResults FindCards(CardSearchParameters searchParameters, bool loadDetails)
        {
            CardSearchResults results = new CardSearchResults();

            //If an id was passed in we can bypass the full search
            if (!String.IsNullOrEmpty(searchParameters.MultiverseID))
            {
                return FindCardsById(searchParameters.MultiverseID, loadDetails);
            }

            string fullUrl = GetSearchUrl(searchParameters);
            string page = GetPage(fullUrl);
            int maxPages = GetPageCount(page);

            //Set the current page and total number of pages on the result object
            if (maxPages < 1)
            {
                //there was no paging controls, meaning there was only 1 page of results.
                results.Pages = 0;
                results.Page = 0;
            }
            else
            {
                
                results.Pages = maxPages;
                
                //the current page should be the same page the caller requested. However, we need to do some checks just in case
                //an invalid page was submitted in the search.
                if (searchParameters.Page < 0)
                {
                    results.Page = 0;
                }
                else if (searchParameters.Page > maxPages)
                {
                    results.Page = maxPages;
                }
                else
                {
                    results.Page = searchParameters.Page;
                }
            }

            results.TotalCards = -1;

            LoadBaseCardsFromListing(page, results.Cards);

            /* We no longer loop through all the pages and keep retrieving. The caller now needs to explicitly request each page. */
            //int pageNumber = 1;
            //while (page.Contains(String.Format("Default.aspx?page={0}&", pageNumber)))
            //{
            //    fullUrl = GetSearchUrl(searchParameters, pageNumber);
            //    page = GetPage(fullUrl);
            //    LoadBaseCardsFromListing(page, results.Cards);
            //    pageNumber++;
            //}

            if (loadDetails)
            {
                LoadCardDetails(results.Cards);
            }

            return results;
        }

        public override void LoadCardDetails(List<Card> cards)
        {
            cards.ForEach(card => LoadCardDetails(card));
        }

        private string GetCardText(string page)
        {
            StringBuilder returnValue = new StringBuilder();
            string line;
            int startPos = 0;
            while ((line = GetEnclosedHtmlSection(_cardTextStartRegEx, _divEndRegEx, page, ref startPos)) != null)
            {
                returnValue.AppendLine(line);
            }

            return returnValue.ToString();
        }
           

        private string GetEnclosedHtmlSection(string startRegularExpression, string endRegularExpression, string page)
        {
            int pos = 0;
            string value = GetEnclosedHtmlSection(startRegularExpression, endRegularExpression, page, ref pos);

            if (value == null) { value = String.Empty; }

            return value;
        }

        private string GetEnclosedHtmlSection(string startRegularExpression, string endRegularExpression, string page, ref int pos)
        {
            Regex startRegEx = new Regex(startRegularExpression);
            Regex endRegEx = new Regex(endRegularExpression);

            Match startMatch = startRegEx.Match(page, pos);
            if (startMatch.Success)
            {
                int startPos = startMatch.Index + startMatch.Length;

                Match endMatch = endRegEx.Match(page, startPos);
                if (endMatch.Success)
                {
                    pos = endMatch.Index + endMatch.Length;
                    return page.Substring(startPos, endMatch.Index - startPos);
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }

        public override void LoadCardDetails(Card card)
        {
            string fullUrl = _detailUrl + card.MultiverseID;
            string page = GetPage(fullUrl);
            card.Name = GetValueFromRegEx(_nameRegEx, page);
            card.ConvertedManaCost = GetValueFromRegEx(_convertedManaCostRegEx, page);
            card.CardTypes = GetValueFromRegEx(_typesRegEx, page);
            card.Power = GetValueFromRegEx(_powerRegEx, page);
            card.Toughness = GetValueFromRegEx(_toughnessRegEx, page);
            card.Rarity = GetValueFromRegEx(_rarityRegEx, page);
            card.Expansion = GetValueFromRegEx(_expansionRegEx, page);
            card.SetImage(GetImage(String.Format(_cardImageUrl, card.MultiverseID)));
            card.ManaCost = ReplaceImagesWithAltText(GetEnclosedHtmlSection(_manaCostStartRegEx, _divEndRegEx, page));
            card.CardText = ReplaceImagesWithAltText(GetCardText(page));
        }

        private System.IO.Stream GetImage(string imageUrl)
        {
            Uri ImageURI = new Uri(imageUrl);
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ImageURI);
            return request.GetResponse().GetResponseStream();
        }

        private string ReplaceImagesWithAltText(string value)
        {
            Regex regex = new Regex(_imageAltTextRegEx);
            string tempValue = regex.Replace(value, _imageAltReplacementRegEx);
            tempValue = tempValue.Replace(_chaosImageReplacement, "Chaos");
            //The replacement adds a space to the end of the Alt Text in case there were two images
            //directly next to each other. We now need to get rid of any trailing spaces or double spaces
            //this may have caused.
            tempValue = tempValue.Trim();
            tempValue = tempValue.Replace("  ", " ");

            return tempValue;
        }

        private int GetPageCount(string page)
        {
            Regex regex = new Regex(_pageLinksParameter);

            int maxPage = 0;

            foreach (Match match in regex.Matches(page))
            {
                int currentPage;
                if (Int32.TryParse(match.Groups[1].Value, out currentPage))
                {
                    if (currentPage > maxPage)
                    {
                        maxPage = currentPage;
                    }
                }
            }

            return maxPage;
        }

        private void LoadBaseCardsFromListing(string page, List<Card> cards)
        {
            Regex regex = new Regex(_multiverseIdRegEx);

            foreach (Match match in regex.Matches(page))
            {
                if (match.Success)
                {
                    string id = match.Groups[1].Value;

                    //First ensure it's not one of the basic lands we want to exclude
                    if (!_basicLandExclusionList.Contains(id))
                    {
                        Card card = new Card { MultiverseID = id, DetailsLoaded = false };
                        if (!cards.Contains(card)) { cards.Add(card); }
                    }
                }
            }
        }

        private string GetValueFromRegEx(string regularExpression, string valueToSearch)
        {
            Regex regex = new Regex(regularExpression);
            Match match = regex.Match(valueToSearch);

            if (match.Success)
            {
                return match.Groups[1].Value;
            }
            else
            {
                return String.Empty;
            }
        }

        private string GetPage(string url)
        {
            CookieContainer container = new CookieContainer();
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            container.Add(new Cookie("CardDatabaseSettings", "0=1&1=28&2=0&14=1&3=13&4=0&5=1&6=15&7=0&8=1&9=1&10=19&11=4&12=8&15=1&16=1&13=", "/", "gatherer.wizards.com"));
            request.CookieContainer = container;
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            using (StreamReader reader = new StreamReader(response.GetResponseStream()))
            {
                return reader.ReadToEnd();
            }
        }

        public override Gatherer Clone()
        {
            return new WebGatherer();
        }
    }
}
